Dynamic Templates

The previous tutorial covered how you can parameterise a template, allowing you to change variables on the fly.

While this works great for smaller scripts, there is more power to be leveraged here with “Dynamic” variables.

These allow you to “link” parameters together, so updating one value can make much larger changes to the script automatically.

Linked Parameters

Linking parameters together is simple to do.

When we covered kwargs in the previous tutorial, we also described default and value.

These allow you to specify python code within them, which will be evaluated.

The syntax used here is based on the python f-string syntax.

f-strings

f-strings are a feature in python that allows strings to be “evaluated” at runtime.

As a simple example, the string f"value={foo}" will evaluate using the value of foo. If we set foo=True, print(f"value={foo}") will return value=True.

Note

In short, anything within {curly brackets} will be evaluated as though it is python code.

Warning

Code within a dynamic value is executed via eval on your local machine, so you should be aware of the security implications of this. Make sure that you’re aware of any code that may be running when you generate a jobscript!

Lets demonstrate this by setting up nodes to be dependent on mpi, omp and a new cores_per_node variable.

[2]:
from remotemanager import Computer
[3]:
template = """#!/bin/bash

#SBATCH --ntasks=#NTASKS#
#SBATCH --cpus-per-task=#CPUS_PER_TASK#
#SBATCH --nodes=#NODES:default={ntasks*cpus_per_task/cores_per_node}#
#SBATCH --queue=#QUEUE#
#SBATCH --account=#ACCOUNT#
#SBATCH --walltime=#TIME:format=time:default=3600#
#SBATCH --exclusive

# using cores_per_node: #CORES_PER_NODE:default=128#

#MODULES#"""
[4]:
test = Computer(template=template)
[5]:
test.ntasks = 1024
test.cpus_per_task = 4

print(test.script())
#!/bin/bash

#SBATCH --ntasks=1024
#SBATCH --cpus-per-task=4
#SBATCH --nodes=32
#SBATCH --walltime=01:00:00
#SBATCH --exclusive

# using cores_per_node: 128

Extra Variables

All variables that are required within the script must be available within the Computer itself.

The easiest way of doing this is to add a “commented” line to the template.

For example, we had to add cores_per_node to the script, itself.

By doing this, you expose it as a value to the Computer, and add clarity for users.

Input Order

One thing to note with the previous example is that the order of your variables do not matter.

Since the values are evaluated when the script is generated, you can link values in any direction.

In this case, nodes is dependent on the cores_per_node value, which comes after it in the script.

Chaining Variables

Variables can also be “chained” into one another.

In this example, we are generating a default jobname that depends on the nodes. Nodes, in turn, depends on other variables.

[6]:
template = """#!/bin/bash

#SBATCH --ntasks=#NTASKS#
#SBATCH --cpus-per-task=#CPUS_PER_TASK#
#SBATCH --nodes=#NODES:default={ntasks*cpus_per_task/cores_per_node}#
#SBATCH --walltime=#TIME:format=time:default=3600#
#SBATCH --jobname=#JOBNAME:default=RUN_{ntasks}_{cpus_per_task}_{nodes}#
#SBATCH --exclusive

# using cores_per_node: #CORES_PER_NODE:default=128#

#MODULES#"""
[7]:
test = Computer(template=template)
[8]:
test.ntasks = 256
test.cpus_per_task = 4

print(test.script())
#!/bin/bash

#SBATCH --ntasks=256
#SBATCH --cpus-per-task=4
#SBATCH --nodes=8
#SBATCH --walltime=01:00:00
#SBATCH --jobname=RUN_256_4_8
#SBATCH --exclusive

# using cores_per_node: 128

Iterables

Added in version 0.13.5.

You are also able to specify limited iterables within your values, this can be useful for “selecting” arguments within jobscripts.

Similar to the quotation method of escaping control characters, values in {evaluation} blocks are also escaped. This allows the specification of dictionaries.

Lets set up a template that requests a larger partition if there are too many nodes.

[9]:
template = """#!/bin/bash

#SBATCH --ntasks=#NTASKS#
#SBATCH --cpus-per-task=#CPUS_PER_TASK#
#SBATCH --nodes=#NODES:default={ntasks*cpus_per_task/cores_per_node}#
#SBATCH --partition=#partition:default={partition_table[nodes<16]}#

# using cores_per_node: #CORES_PER_NODE:default=128#
#partition_table:default={{True: "small", False: "large"}}:hidden=True#
"""

test = Computer(template=template)

Note

We are making use of the hidden variable here to reduce clutter.

Now if we generate a script that only requests 2 nodes, we would expect the “small” partition to be requested:

[10]:
print(test.script(ntasks=64, cpus_per_task=4))
#!/bin/bash

#SBATCH --ntasks=64
#SBATCH --cpus-per-task=4
#SBATCH --nodes=2
#SBATCH --partition=small

# using cores_per_node: 128

Now, if we generate a much larger job, it should request the “large” partition automatically

[11]:
print(test.script(ntasks=1024, cpus_per_task=4))
#!/bin/bash

#SBATCH --ntasks=1024
#SBATCH --cpus-per-task=4
#SBATCH --nodes=32
#SBATCH --partition=large

# using cores_per_node: 128

Note

This functionality is not limited to dicts. (Tested to be working with lists and tuples).

Dynamics and Escape Sequences

As mentioned previously, control characters like :, = can be escaped either by quoting, or by using the escape charater \.

The resulting string will continue to function with dynamic values:

[12]:
template = """#!/bin/bash

#SBATCH --ntasks=#NTASKS#
#SBATCH --cpus-per-task=#CPUS_PER_TASK#
#SBATCH --nodes=#NODES:default={ntasks*cpus_per_task/cores_per_node}#

# using cores_per_node: #CORES_PER_NODE:default=128:hidden=True#

# tasks to nodes ratio: #ratio:default={ntasks}\\:{nodes}#
"""

test = Computer(template=template)
[13]:
print(test.script(ntasks=64, cpus_per_task=4))
#!/bin/bash

#SBATCH --ntasks=64
#SBATCH --cpus-per-task=4
#SBATCH --nodes=2


# tasks to nodes ratio: 64:2

Escaping Evaluation

Using the backslash escape technique is the only method for escaping the {} evaluation pattern within values.

[14]:
template = r"""srcdir = #src:default=test#
dir = #dir:default=$\{HOME\}/{src}#"""

test = Computer(template=template)
[15]:
print(test.script())
srcdir = test
dir = ${HOME}/test

Try this without the escape \, you’ll notice that the script production will fail, as it’s expecting a home parameter.

Summary

Taking into account the previous two tutorials, you should now have all the tools needed to generate suitable jobscripts for any machine.

To summarise the steps:

  1. Obtain a valid jobscript. This can be either from a job that you know has run (either yours or another user’s), the documentation, or the machine helpdesk.

  2. Identify the parts of this script that you would like to have control over. ntasks, nodes, etc.

  3. Convert the static parameters that the script has into sensible parameter names.

  4. Load the template into a Computer

  5. Dataset can now use this object to generate scripts based on the arguments that you give.

Tip

Remember that you can debug your script at any point manually by printing the output of the script() method.